Skip to content

汎用 locale lib(hooks/lib/locale.ts)を追加し賛辞語彙を最初の消費者にする - #4

Merged
takanorinishida merged 1 commit into
developfrom
feature/i18n/locale-core
Jul 30, 2026
Merged

汎用 locale lib(hooks/lib/locale.ts)を追加し賛辞語彙を最初の消費者にする#4
takanorinishida merged 1 commit into
developfrom
feature/i18n/locale-core

Conversation

@takanorinishida

Copy link
Copy Markdown
Owner

何をするか

hooks/lib/work-strings.ts(PR danielmiessler#1695)の設計契約(キー単位フォールバック、fail-open、paiUserDir() 経由の locale パス)を汎用 lib に一般化する。USER/FORK/GOALS.md で最初に出すと決めていた PR。

設計

2つの lookup 形を用意した — 散文と語彙集合で正しいフォールバック意味論が逆だから。

  • t(lang, key, fallback, vars) — OVERRIDE。locale が翻訳を持つキーは呼び出し元の default を上書きする。汎用 lib なので固定 EN 辞書は持たない(work-strings.ts は Work System issue 本文専用のキー空間を正当に所有しているので別)
  • tList(lang, key) — UNION。locale の配列値は呼び出し元自身のリストに足す、置き換えない。日本語 locale を使っても英語話者の "perfect" は賛辞のままであるべき。これにより「locale 未設定 → 出力不変」が規約ではなく構造的に証明できる — バンドルが無ければ tList()[] を返し、呼び出し元のリストは触られない

resolveLang() の優先順位: 明示引数 > LIFEOS_CONFIG.toml の新設 [principal].language(optional) > WORK.ISSUE_LANGUAGEwork-config.ts の非 export loadIssueLanguage() には依存せず独立実装 — lib を Work System 内部に依存させないため) > "en"。全段階 try/catch でガードし、絶対に例外を投げない。

最初の消費者

SatisfactionCapture.hook.ts の賛辞 fast-path。POSITIVE_PRAISE_WORDS/POSITIVE_PHRASES はそのまま維持し、EFFECTIVE_PRAISE_WORDS/EFFECTIVE_PRAISE_PHRASEStList(lang, "praise.words"/"praise.phrases") が非空のときだけ union する。locale 未設定なら同一 Set オブジェクト参照(コピーですらない)。あわせて正規化の句読点除去に全角 CJK 句読点(。、!?…「」)を追加 — 英語プロンプトにはこれらの文字が現れないので no-op、「完璧!」が「完璧」に正規化されて一致するようになる。

非破壊の根拠

削除行は全て「同じ正規化目的の、より広い版に置き換わった正規表現行」か「locale 未設定なら元の Set そのものを指す参照に変わった行」のいずれか。LifeosConfig.ts の変更は完全に追加のみ(optional フィールド1個 + それを通す1行)で削除行ゼロ。

検証(隔離環境の LIFEOS_CONFIG_PATH/LIFEOS_DIR probe harness、PR danielmiessler#1703 と同スタイル)

  • (a) locale 未設定: perfect/great job/nice/now do X/8 が変更前と完全一致。日本語賛辞(完璧/ありがとう)は正しく発火しない(語彙未設定=英語のみのベースラインと一致)
  • (b) [principal].language = "ja" + ja.json に praise.words/phrases: 英語賛辞は引き続き発火(union であって置換ではない)。完璧!/ありがとう/助かった が新たに rating 8 で発火。非賛辞の日本語(次はどうする/これを直して)は正しく発火しない
  • (c) WORK.ISSUE_LANGUAGE: ja[principal].language 未設定): フォールバック連鎖の3段目が独立に機能し、同じ語彙が有効になることを確認
  • (d) 壊れた locale JSON: 例外なし、exit 0、英語の挙動が維持される(fail-open で tList()[] を返す)

なお、このブランチを単独で見ると 2はあとで が今も rating 2 に誤検出されるが、これは PR danielmiessler#1703(すでに develop にマージ済み)で直したバグで、本ブランチは main から切ったため含まれていないだけ。触っている箇所が完全に別区画(レーティングガード vs 賛辞語彙参照)なのでマージ時のコンフリクトは想定していない。

🤖 Generated with Claude Code

…o SatisfactionCapture

Generalizes work-strings.ts's design contract (LifeOS#1695: key-level
fallback, fail-open, paiUserDir()-resolved locale files) into a reusable
lib any surface can adopt, per USER/FORK/GOALS.md's stated plan for
i18n goal 2 ("Work System の hooks/lib/work-strings.ts の設計契約...を
汎用 locale lib に一般化する PR を最初に出す").

Two lookup shapes, because prose and vocabulary need opposite fallback
semantics — this is the actual design contribution here, not just a
rename of work-strings' machinery:

- `t(lang, key, fallback, vars)` — OVERRIDE. A locale bundle replaces
  the caller's default for a key it translates. No baked-in EN
  dictionary (unlike work-strings.ts, which legitimately owns one
  scoped to Work System issue bodies) — the caller supplies its own
  default, since a generic lib has no single key-space to own.
- `tList(lang, key)` — UNION. A locale bundle's array values are ADDED
  to the caller's own list, never replacing it. A Japanese locale
  should still recognize "perfect" as praise, not lose English
  vocabulary by opting in — this is what makes "locale unset → output
  unchanged" provable by construction rather than by convention: with
  no bundle, tList() returns [] and the caller's list is untouched.

resolveLang() priority: explicit arg > LIFEOS_CONFIG.toml
[principal].language (new optional field, LifeosConfig.ts) >
WORK.ISSUE_LANGUAGE (read independently, not by importing
work-config.ts's unexported loadIssueLanguage() — this keeps
locale.ts standalone rather than reaching into Work System internals
for one value) > "en". Every step is try/catch-guarded and never
throws, same fail-open contract work-strings.ts uses for locale
bundles.

First consumer: SatisfactionCapture.hook.ts's positive-praise fast-path.
Its POSITIVE_PRAISE_WORDS/POSITIVE_PHRASES stay exactly as they are —
EFFECTIVE_PRAISE_WORDS/EFFECTIVE_PRAISE_PHRASES union in tList(lang,
"praise.words"/"praise.phrases") only when non-empty, so with no locale
configured they're the literal same Set object, not a copy. Also widens
the fast-path's punctuation-stripping regex to full-width CJK
punctuation (。、!?…「」) alongside the existing ASCII set — English
prompts never contain these characters, so this is a no-op for them;
it's what lets "完璧!" normalize to "完璧" and match the locale
vocabulary.

Every deletion in this diff is either a regex line replaced by a wider
version of itself (same normalization purpose) or a Set-reference read
that now points at a value which, absent locale configuration, IS the
original Set (not a copy) — same "dispatch line branching to the
original" pattern used across this fork's other PRs, applied to a value
reference instead of a function call. LifeosConfig.ts's changes are
purely additive (one optional interface field, one line threading it
through validateAndNormalize) — zero deletions.

Verified with an isolated LIFEOS_CONFIG_PATH/LIFEOS_DIR probe harness
(same style as the ASCII-guard fix):

(a) locale unset (no [principal].language, no WORK/config.yaml, no
    locale file) — perfect/great job/nice/now do X/8 produce byte-
    identical output to pre-change; Japanese praise ("完璧", "ありがとう")
    correctly does NOT fire (no vocabulary configured, matches
    English-only baseline)
(b) [principal].language = "ja" + USER/CONFIG/locales/ja.json with
    praise.words/praise.phrases — English praise still fires
    (union, not replacement); "完璧!", "ありがとう", "助かった" now fire
    rating 8; non-praise Japanese ("次はどうする", "これを直して")
    correctly produces no rating
(c) WORK.ISSUE_LANGUAGE: ja (config.yaml) with no [principal].language
    set — confirms the fallback chain's third tier resolves and
    activates the same vocabulary independently of the toml field
(d) malformed locale JSON — no throw, exit 0, English behavior
    preserved (bundle load fails closed, tList() returns [])

Note: "2はあとで" still misreads as rating 2 on this branch in isolation
— that's the bug fix/satisfaction-rating-ascii-guard (danielmiessler#1703) already
closed, on a branch cut from `develop` after that fix landed there; this
branch was cut from `main`, which doesn't have it yet. No conflict
expected on merge — the two changes touch disjoint regions of the file
(explicit-rating guards vs. praise-vocabulary lookup).
@takanorinishida
takanorinishida merged commit 880896f into develop Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant